Current File : /home/jeconsul/public_html/wp-content/plugins/suremails/inc/emails/handler/uploads.php
<?php
/**
 * Uploads.php
 *
 * Provides methods for handling file operations for email attachments and uploads.
 *
 * @package SureMails\Inc\Utilities
 */

namespace SureMails\Inc\Emails\Handler;

use SureMails\Inc\Traits\Instance;
use WP_Error;

/**
 * Class Uploads
 *
 * Provides methods for handling file operations for email attachments and uploads.
 */
class Uploads {

	use Instance;

	/**
	 * Directory name for SureMails uploads.
	 *
	 * @since 1.5.0
	 */
	public const BASE_FOLDER = 'suremails';

	/**
	 * Process and update a list of file attachments.
	 *
	 * @since 1.5.0
	 *
	 * @param array $attachments List of attachment information.
	 * @return array Modified attachment list.
	 */
	public function handle_attachments( $attachments ) {
		return array_map(
			function ( $attachment ) {
				// Unpack our attachment parameters.
				$path      = $attachment;
				$filename  = wp_basename( $path );
				$is_string = false;

				$new_path = $this->handle_single_attachment( $path, $filename, $is_string );
				if ( ! empty( $new_path ) ) {
					$attachment = $new_path;
				}

				return $attachment;
			},
			$attachments
		);
	}

	/**
	 * Retrieve the base SureMails uploads directory.
	 *
	 * @since 1.5.0
	 *
	 * @return array|WP_Error Array with 'path' and 'url' or an error.
	 */
	public static function get_suremails_base_dir() {
		$upload_info = wp_upload_dir();
		if ( ! empty( $upload_info['error'] ) ) {
			return new WP_Error( 'suremails_upload_dir_error', $upload_info['error'] );
		}

		$folder = self::BASE_FOLDER;

		$base_dir = realpath( $upload_info['basedir'] );
		if ( $base_dir === false ) {
			return new WP_Error( 'suremails_upload_dir_error', __( 'Invalid upload base directory.', 'suremails' ) );
		}
		$base   = trailingslashit( $base_dir ) . $folder;
		$custom = apply_filters( 'suremails_uploads_base_dir', $base );

		if ( wp_is_writable( $custom ) ) {
			$base = $custom;
		}

		if ( ! file_exists( $base ) && ! wp_mkdir_p( $custom ) ) {
			return new WP_Error(
				'suremails_upload_dir_create_failed',
				// translators: %s is the directory path.
				sprintf( __( 'Cannot create directory %s. Check parent directory permissions.', 'suremails' ), esc_html( $base ) )
			);
		}

		if ( ! wp_is_writable( $custom ) ) {
			return new WP_Error(
				'suremails_upload_dir_not_writable',
				// translators: %s is the directory path.
				sprintf( __( 'Directory %s is not writable.', 'suremails' ), esc_html( $base ) )
			);
		}

		return [
			'path' => $base,
			'url'  => trailingslashit( $upload_info['baseurl'] ) . $folder,
		];
	}

	/**
	 * Generate an .htaccess file to secure the uploads folder.
	 *
	 * @since 1.5.0
	 *
	 * @return bool True on success, false on failure.
	 */
	public static function generate_htaccess_file() {
		$base_dir = self::get_suremails_base_dir();
		if ( is_wp_error( $base_dir ) ) {
			return false;
		}

		$ht_file = wp_normalize_path( trailingslashit( $base_dir['path'] ) . '.htaccess' );
		$content = apply_filters(
			'suremails_htaccess_content',
			'# Disable PHP and Python script execution.
<Files *>
  SetHandler none
  SetHandler default-handler
  RemoveHandler .cgi .php .php3 .php4 .php5 .phtml .pl .py .pyc .pyo
  RemoveType .cgi .php .php3 .php4 .php5 .phtml .pl .py .pyc .pyo
</Files>
<IfModule mod_php5.c>
  php_flag engine off
</IfModule>
<IfModule mod_php7.c>
  php_flag engine off
</IfModule>
<IfModule mod_php8.c>
  php_flag engine off
</IfModule>
<IfModule headers_module>
  Header set X-Robots-Tag "noindex"
</IfModule>'
		);
		if ( function_exists( 'insert_with_markers' ) === false ) {
			require_once ABSPATH . 'wp-admin/includes/misc.php';
		}
		return insert_with_markers( $ht_file, 'SureMails', $content );
	}

	/**
	 * Create an empty index.html file in the specified folder if missing.
	 *
	 * @since 1.5.0
	 *
	 * @param string $folder_path The directory in which to create the file.
	 * @return int|false Number of bytes written or false on failure.
	 */
	public static function generate_index_html( $folder_path ) {
		if ( ! is_dir( $folder_path ) || is_link( $folder_path ) ) {
			return false;
		}
		$index = wp_normalize_path( trailingslashit( $folder_path ) . 'index.html' );
		// If the index file already exists, do nothing.
		if ( file_exists( $index ) ) {
			return false;
		}

		// Initialize the WP Filesystem.
		if ( ! function_exists( 'WP_Filesystem' ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
		}
		global $wp_filesystem;
		if ( empty( $wp_filesystem ) ) {
			WP_Filesystem();
		}
		return $wp_filesystem->put_contents( $index, '' );
	}

	/**
	 * Process a single attachment file by obfuscating its path and saving its data.
	 *
	 * @since 1.5.0
	 *
	 * @param string $path       The original file path or content.
	 * @param string $filename   The original file name.
	 * @param bool   $is_string  Indicates if the attachment is provided as a string.
	 * @return string|false New file path if stored successfully; false otherwise.
	 */
	private function handle_single_attachment( $path, $filename = '', $is_string = false ) {
		$content = $this->fetch_attachment_content( $path, $is_string );
		if ( $content === false ) {
			return false;
		}

		// When not a string-based attachment, use the original file name if none provided.
		if ( ! $is_string && $filename === '' ) {
			$filename = wp_basename( $path );
		}

		$filename = sanitize_file_name( $filename );
		$stored   = $this->save_file( $content, $filename );
		return empty( $stored ) ? $path : $stored;
	}

	/**
	 * Retrieve the content from a given file or string.
	 *
	 * @since 1.5.0
	 *
	 * @param string $source    The file path or the file content.
	 * @param bool   $is_string Whether the source is a string of content.
	 * @return string|false File content or false on failure.
	 */
	private function fetch_attachment_content( $source, $is_string ) {
		if ( ! $is_string ) {
			if ( ! is_readable( $source ) ) {
				return false;
			}
			return file_get_contents( $source );
		}
		return $source;
	}

	/**
	 * Save file content to a new location inside the SureMails uploads directory.
	 *
	 * @since 1.5.0
	 *
	 * @param string $content       The file data.
	 * @param string $original_name The original file name.
	 * @return string|false New file path on success or false on failure.
	 */
	private function save_file( $content, $original_name ) {
		$upload_dir = $this->get_uploads_folder();

		if ( is_wp_error( $upload_dir ) ) {
			return false;
		}

		if ( ! is_dir( $upload_dir ) ) {
			wp_mkdir_p( $upload_dir );

			// Create security and index files in the upload directories.
			self::generate_htaccess_file();
			$base_dir = Uploads::get_suremails_base_dir();
			if ( ! is_wp_error( $base_dir ) && isset( $base_dir['path'] ) ) {
				self::generate_index_html( $base_dir['path'] );
			}
			self::generate_index_html( $upload_dir );
		}

		// Get the extension from the original file name.
		$extension = pathinfo( $original_name, PATHINFO_EXTENSION );

		// Compute a hash of the content.
		$hash = hash( 'md5', $content );

		$new_name = substr( $hash, 0, 16 ) . '-' . basename( $original_name );

		$upload_dir = trailingslashit( $upload_dir );
		$new_path   = $upload_dir . $new_name;

		if ( is_file( $new_path ) ) {
			return $new_path;
		}

		// Ensure the upload directory is writable using the WP helper.
		if ( ! wp_is_writable( $upload_dir ) ) {
			return false;
		}

		// Initialize the WP Filesystem.
		if ( ! function_exists( 'WP_Filesystem' ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
		}
		global $wp_filesystem;
		if ( empty( $wp_filesystem ) ) {
			WP_Filesystem();
		}

		$result = $wp_filesystem->put_contents( $new_path, $content, FS_CHMOD_FILE );
		return $result ? $new_path : false;
	}

	/**
	 * Get the SureMails-specific uploads folder.
	 *
	 * @since 1.5.0
	 *
	 * @return string|WP_Error The absolute folder path or error.
	 */
	private function get_uploads_folder() {
		$base = self::get_suremails_base_dir();
		if ( is_wp_error( $base ) ) {
			return $base;
		}
		return trailingslashit( trailingslashit( $base['path'] ) . 'attachments' );
	}
}